//! Parsing / deserialization / decoding

use crate::{
    agents::ForAgent, commit::CommitOpts, datatype::DataType, errors::AtomicResult,
    resources::PropVals, urls, utils::check_valid_url, values::SubResource, AtomicError, Resource,
    Storelike, Value,
};

pub const JSON_AD_MIME: &str = "application/ad+json";

pub fn parse_json_array(string: &str) -> AtomicResult<Vec<String>> {
    let vector: Vec<String> = serde_json::from_str(string)?;
    Ok(vector)
}

use serde_json::Map;

/// Options for parsing (JSON-AD) resources.
/// Many of these are related to rights, as parsing often implies overwriting / setting resources.
#[derive(Debug, Clone)]
pub struct ParseOpts {
    /// URL of the parent / Importer. This is where all the imported data will be placed under, hierarchically.
    /// If imported resources do not have an `@id`, we create new `@id` using the `localId` and the `parent`.
    /// If the importer resources already have a `parent` set, we'll use that one.
    pub importer: Option<String>,
    /// Who's rights will be checked when creating the imported resources.
    /// Is only used when `save` is set to [SaveOpts::Commit].
    /// If [None] is passed, all resources will be
    pub for_agent: ForAgent,
    /// Who will perform the importing. If set to none, all possible commits will be signed by the default agent.
    /// Note that this Agent needs a private key to sign the commits.
    /// Is only used when `save` is set to `Commit`.
    pub signer: Option<crate::agents::Agent>,
    /// How you want to save the Resources, if you want to add Commits for every Resource.
    pub save: SaveOpts,
    /// Overwrites existing resources with the same `@id`, even if they are not children of the `importer`.
    /// This can be a dangerous value if true, because it can overwrite _all_ resources where the `for_agen` has write rights.
    /// Only parse items from sources that you trust!
    pub overwrite_outside: bool,
}

#[derive(Debug, Clone, PartialEq)]
pub enum SaveOpts {
    /// Don't save the parsed resources to the store.
    /// No authorization checks will be performed.
    DontSave,
    /// Save the parsed resources to the store, but don't create Commits for every change.
    /// Removes existing properties that are not present in the imported resource.
    /// Does not perform authorization checks.
    Save,
    /// Create Commits for every change.
    /// Does not remove existing properties.
    /// Performs authorization cheks (if enabled)
    Commit,
}

impl std::default::Default for ParseOpts {
    fn default() -> Self {
        Self {
            signer: None,
            importer: None,
            for_agent: ForAgent::Sudo,
            overwrite_outside: true,
            save: SaveOpts::Save,
        }
    }
}

/// Parse a single Json AD string, convert to Atoms
/// WARNING: Does not match all props to datatypes (in Nested Resources),
/// so it could result in invalid data, if the input data does not match the required datatypes.
#[tracing::instrument(skip(store))]
pub fn parse_json_ad_resource(
    string: &str,
    store: &impl crate::Storelike,
    parse_opts: &ParseOpts,
) -> AtomicResult<Resource> {
    let json: Map<String, serde_json::Value> = serde_json::from_str(string)?;
    parse_json_ad_map_to_resource(json, store, None, parse_opts)
}

fn object_is_property(object: &serde_json::Value) -> bool {
    if let serde_json::Value::Object(map) = object {
        if let Some(serde_json::Value::Array(arr)) = map.get(urls::IS_A) {
            for item in arr {
                if let serde_json::Value::String(s) = item {
                    if s == urls::PROPERTY {
                        return true;
                    }
                }
            }
        }
    }
    false
}

fn get_parent_to_pull(value: &serde_json::Value) -> Option<String> {
    if let serde_json::Value::Object(map) = value {
        if let Some(serde_json::Value::String(s)) = map.get(urls::PARENT) {
            if check_valid_url(s).is_err() {
                return Some(s.to_string());
            }
        }
    }
    None
}

fn find_parent_in_array(
    parent_subject: &str,
    array: &Vec<serde_json::Value>,
) -> Option<serde_json::Value> {
    for value in array {
        let serde_json::Value::Object(object) = value else {
            continue;
        };

        let Some(serde_json::Value::String(s)) = object.get(&urls::LOCAL_ID.to_string()) else {
            continue;
        };

        if s == parent_subject {
            return Some(value.clone());
        }
    }
    None
}

/// Loops over the array, for each property check if their parent is a local_id. If true find the parent and move it to the front of the array.
fn pull_parents_of_props_to_front(array: &Vec<serde_json::Value>) -> Vec<serde_json::Value> {
    let mut new_vec: Vec<serde_json::Value> = Vec::new();

    for value in array {
        if object_is_property(value) {
            if let Some(parent_subject) = get_parent_to_pull(value) {
                if let Some(parent) = find_parent_in_array(&parent_subject, array) {
                    new_vec.insert(0, parent);
                }
            }
        }

        if !new_vec.contains(value) {
            new_vec.push(value.clone());
        }
    }

    new_vec
}

/// Parses JSON-AD string.
/// Accepts an array containing multiple objects, or one single object.
#[tracing::instrument(skip(store))]
pub fn parse_json_ad_string(
    string: &str,
    store: &impl Storelike,
    parse_opts: &ParseOpts,
) -> AtomicResult<Vec<Resource>> {
    let parsed: serde_json::Value = serde_json::from_str(string)
        .map_err(|e| AtomicError::parse_error(&format!("Invalid JSON: {}", e), None, None))?;
    let mut vec = Vec::new();
    match parsed {
        serde_json::Value::Array(mut arr) => {
            // Move all properties to the front of the array because some of the other resouces might use these properties.
            arr.sort_by(|a, b| {
                let a_is_prop = object_is_property(a);
                let b_is_prop = object_is_property(b);
                b_is_prop.cmp(&a_is_prop)
            });

            // Also move the parents of the properties to the front when they are included in the data.
            arr = pull_parents_of_props_to_front(&arr);

            for item in arr {
                match item {
                    serde_json::Value::Object(obj) => {
                        let resource = parse_json_ad_map_to_resource(obj, store, None, parse_opts)
                            .map_err(|e| format!("Unable to process resource in array. {}", e))?;
                        vec.push(resource);
                    }
                    wrong => {
                        return Err(
                            format!("Wrong datatype, expected object, got: {:?}", wrong).into()
                        )
                    }
                }
            }
        }
        serde_json::Value::Object(obj) => vec.push(
            parse_json_ad_map_to_resource(obj, store, None, parse_opts)
                .map_err(|e| format!("Unable to parse object. {}", e))?,
        ),
        _other => return Err("Root JSON element must be an object or array.".into()),
    }

    Ok(vec)
}

/// Parse a single Json AD string that represents an incoming Commit.
/// WARNING: Does not match all props to datatypes (in Nested Resources), so it could result in invalid data,
/// if the input data does not match the required datatypes.
#[tracing::instrument(skip(store))]
pub fn parse_json_ad_commit_resource(
    string: &str,
    store: &impl crate::Storelike,
) -> AtomicResult<Resource> {
    let json: Map<String, serde_json::Value> = serde_json::from_str(string)?;
    let signature = json
        .get(urls::SUBJECT)
        .ok_or("No subject field in Commit.")?
        .to_string();

    // Incoming commits do not have an @id field, we generate that from the signature.
    let subject = format!("{}/commits/{}", store.get_server_url()?, signature);

    let resource =
        parse_json_ad_map_to_resource(json, store, Some(subject), &ParseOpts::default())?;

    Ok(resource)
}

/// Converts a string to a URL (subject), check for localid
fn try_to_subject(subject: &str, prop: &str, parse_opts: &ParseOpts) -> AtomicResult<String> {
    if check_valid_url(subject).is_ok() {
        Ok(subject.into())
    } else if let Some(importer) = &parse_opts.importer {
        Ok(generate_id_from_local_id(importer, subject))
    } else {
        Err(AtomicError::parse_error(
            &format!("Unable to parse string as URL: {}", subject),
            None,
            Some(prop),
        ))
    }
}

fn parse_anonymous_resource(
    map: &Map<String, serde_json::Value>,
    subject: Option<&str>,
    store: &impl crate::Storelike,
    parse_opts: &ParseOpts,
) -> AtomicResult<PropVals> {
    let mut propvals = PropVals::new();

    for (prop, val) in map {
        if prop == "@id" || prop == urls::LOCAL_ID {
            return Err(AtomicError::parse_error(
                "`@id` and `localId` are not allowed in anonymous resources",
                subject.as_deref(),
                Some(prop),
            ));
        }

        let (updated_key, atomic_val) = parse_propval(prop, val, subject, store, parse_opts)?;
        propvals.insert(updated_key.to_string(), atomic_val);
    }

    Ok(propvals)
}

fn parse_propval(
    key: &str,
    val: &serde_json::Value,
    subject: Option<&str>,
    store: &impl crate::Storelike,
    parse_opts: &ParseOpts,
) -> AtomicResult<(String, Value)> {
    let prop = try_to_subject(&key, &key, parse_opts)?;
    let property = store.get_property(&prop)?;

    let atomic_val: Value = match property.data_type {
        DataType::AtomicUrl => {
            match val {
                serde_json::Value::String(str) => {
                    // If the value is not a valid URL, and we have an importer, we can generate_id_from_local_id
                    let url = try_to_subject(&str, &prop, parse_opts)?;
                    Value::new(&url, &property.data_type)?
                }
                serde_json::Value::Object(map) => {
                    let propvals = parse_anonymous_resource(&map, subject, store, parse_opts)?;
                    Value::NestedResource(SubResource::Nested(propvals))
                }
                _ => {
                    return Err(AtomicError::parse_error(
                        "Invalid value for AtomicUrl, not a string or object",
                        subject.as_deref(),
                        Some(&prop),
                    ));
                }
            }
        }
        DataType::ResourceArray => {
            let serde_json::Value::Array(array) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for ResourceArray, not an array",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            let mut newvec: Vec<SubResource> = Vec::new();
            for item in array {
                match item {
                    serde_json::Value::String(str) => {
                        let url = try_to_subject(&str, &prop, parse_opts)?;
                        newvec.push(SubResource::Subject(url))
                    }
                    // If it's an Object, it can be either an anonymous or a full resource.
                    serde_json::Value::Object(map) => {
                        let propvals = parse_anonymous_resource(&map, subject, store, parse_opts)?;
                        newvec.push(SubResource::Nested(propvals))
                    }
                    err => {
                        return Err(AtomicError::parse_error(
                            &format!("Found non-string item in resource array: {err}."),
                            subject.as_deref(),
                            Some(&prop),
                        ))
                    }
                }
            }
            Value::ResourceArray(newvec)
        }
        DataType::String => {
            let serde_json::Value::String(str) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for String, not a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::String(str.clone())
        }
        DataType::Slug => {
            let serde_json::Value::String(str) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for Slug, not a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&str, &DataType::Slug)?
        }
        DataType::Markdown => {
            let serde_json::Value::String(str) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for Markdown, not a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&str, &DataType::Markdown)?
        }
        DataType::Uri => {
            let serde_json::Value::String(str) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for URI, not a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&str, &DataType::Uri)?
        }
        DataType::Date => {
            let serde_json::Value::String(str) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for Date, not a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&str, &DataType::Date)?
        }
        DataType::Boolean => {
            let serde_json::Value::Bool(bool) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for Boolean, not a boolean",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&bool.to_string(), &DataType::Boolean)?
        }
        DataType::Integer => {
            let serde_json::Value::Number(num) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for Integer, not a number",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&num.to_string(), &DataType::Integer)?
        }
        DataType::Float => {
            let serde_json::Value::Number(num) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for Float, not a number",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&num.to_string(), &DataType::Float)?
        }
        DataType::Timestamp => {
            let serde_json::Value::Number(num) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for Timestamp, not a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(&num.to_string(), &DataType::Timestamp)?
        }
        DataType::JSON => Value::JSON(val.clone()),
        DataType::Unsupported(s) => {
            return Err(AtomicError::parse_error(
                &format!("Unsupported datatype: {s}"),
                subject.as_deref(),
                Some(&prop),
            ));
        }
        DataType::YDoc => {
            let serde_json::Value::Object(map) = val else {
                return Err(AtomicError::parse_error(
                    "Invalid value for YDoc, must be of shape { type: \"ydoc\", data: <base64 string> }",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            let Some(data) = map.get("data") else {
                return Err(AtomicError::parse_error(
                    "Invalid value for YDoc, no data field",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            let serde_json::Value::String(data) = data else {
                return Err(AtomicError::parse_error(
                    "Invalid value for YDoc, data field must be a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };

            Value::new(data.as_str(), &DataType::YDoc)?
        }
    };

    Ok((prop, atomic_val))
}

/// Parse a single Json AD string, convert to Atoms
/// Adds to the store if `add` is true.
#[tracing::instrument(skip(store))]
fn parse_json_ad_map_to_resource(
    json: Map<String, serde_json::Value>,
    store: &impl crate::Storelike,
    overwrite_subject: Option<String>,
    parse_opts: &ParseOpts,
) -> AtomicResult<Resource> {
    let mut propvals = PropVals::new();
    let mut subject = overwrite_subject.clone();

    for (prop, val) in json {
        if prop == "@id" {
            if overwrite_subject.is_some() {
                return Err(AtomicError::parse_error(
                    "`@id` is not allowed in a resource with server generated subject.",
                    subject.as_deref(),
                    Some(&prop),
                ));
            }

            subject = if let serde_json::Value::String(s) = val {
                check_valid_url(&s).map_err(|e| {
                    AtomicError::parse_error(
                        &format!("Unable to parse @id {s}: {e}"),
                        subject.as_deref(),
                        Some(&prop),
                    )
                })?;
                Some(s)
            } else {
                return Err(AtomicError::parse_error(
                    "@id must be a string",
                    subject.as_deref(),
                    Some(&prop),
                ));
            };
            continue;
        } else if prop == urls::LOCAL_ID && parse_opts.importer.is_some() {
            if overwrite_subject.is_some() {
                return Err(AtomicError::parse_error(
                    "`@id` is not allowed in a resource with server generated subject.",
                    subject.as_deref(),
                    Some(&prop),
                ));
            }

            // If the property is a localId we need to set to generate a subject and update the subject value.
            let serde_json::Value::String(local_id) = val else {
                return Err(AtomicError::parse_error(
                    "`localId` must be a string",
                    Some(&val.to_string()),
                    Some(&prop),
                ));
            };

            let parent = parse_opts.importer.as_ref().ok_or_else(|| {
                AtomicError::parse_error(
                    "Encountered `localId`, which means we need a `parent` in the parsing options.",
                    subject.as_deref(),
                    Some(&prop),
                )
            })?;

            subject = Some(generate_id_from_local_id(parent, &local_id));

            continue;
        }

        let (new_key, atomic_val) =
            parse_propval(&prop, &val, subject.as_deref(), store, parse_opts)?;

        // Some of these values are _not correctly matched_ to the datatype.
        propvals.insert(new_key, atomic_val);
    }
    // if there is no parent set, we set it to the Importer
    if let Some(importer) = &parse_opts.importer {
        if !propvals.contains_key(urls::PARENT) {
            propvals.insert(urls::PARENT.into(), Value::AtomicUrl(importer.into()));
        }
    }

    // If there is no subject, we return the propvals as a nested resource
    let Some(subj) = subject else {
        return Err(AtomicError::parse_error(
            "No @id or localId found in resource",
            None,
            None,
        ));
    };

    let r = match &parse_opts.save {
        SaveOpts::DontSave => {
            let mut r = Resource::new(subj);
            r.set_propvals_unsafe(propvals);
            r
        }
        SaveOpts::Save => {
            let mut r = Resource::new(subj);
            r.set_propvals_unsafe(propvals);
            store.add_resource(&r)?;
            r
        }
        SaveOpts::Commit => {
            let mut r = if let Ok(orig) = store.get_resource(&subj) {
                // If the resource already exists, and overwrites outside are not permitted, and it does not have the importer as parent...
                // Then we throw!
                // Because this would enable malicious users to overwrite resources that they shouldn't.
                if !parse_opts.overwrite_outside {
                    let importer = parse_opts.importer.as_deref().unwrap();
                    if !orig.has_parent(store, importer) {
                        Err(
                            format!("Cannot overwrite {subj} outside of importer! Enable `overwrite_outside`"),
                        )?
                    }
                };
                orig
            } else {
                Resource::new(subj)
            };
            for (prop, val) in propvals {
                r.set(prop, val, store)?;
            }
            let signer = parse_opts
                .signer
                .clone()
                .ok_or("No agent to sign Commit with. Either pass a `for_agent` or ")?;
            let commit = r.get_commit_builder().clone().sign(&signer, store, &r)?;

            let opts = CommitOpts {
                validate_schema: true,
                validate_signature: true,
                validate_timestamp: false,
                validate_rights: parse_opts.for_agent != ForAgent::Sudo,
                validate_previous_commit: false,
                validate_for_agent: Some(parse_opts.for_agent.to_string()),
                update_index: true,
            };

            store
                .apply_commit(commit, &opts)
                .map_err(|e| format!("Failed to save {}: {}", r.get_subject(), e))?
                .resource_new
                .unwrap()
        }
    };
    Ok(r.into())
}

fn generate_id_from_local_id(importer_subject: &str, local_id: &str) -> String {
    format!("{}/{}", importer_subject, local_id)
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::Storelike;

    #[test]
    fn parse_and_serialize_json_ad() {
        let store = crate::Store::init().unwrap();
        store.populate().unwrap();
        let json_input = r#"{
            "@id": "https://atomicdata.dev/classes/Agent",
            "https://atomicdata.dev/properties/description": "An Agent is a user that can create or modify data. It has two keys: a private and a public one. The private key should be kept secret. The publik key is for proving that the ",
            "https://atomicdata.dev/properties/isA": [
               "https://atomicdata.dev/classes/Class"
            ],
            "https://atomicdata.dev/properties/recommends": [
              "https://atomicdata.dev/properties/description",
              "https://atomicdata.dev/properties/remove",
              "https://atomicdata.dev/properties/destroy"
            ],
              "https://atomicdata.dev/properties/requires": [
              "https://atomicdata.dev/properties/createdAt",
              "https://atomicdata.dev/properties/name",
              "https://atomicdata.dev/properties/publicKey"
            ],
            "https://atomicdata.dev/properties/shortname": "agent"
          }"#;
        let resource = parse_json_ad_resource(json_input, &store, &ParseOpts::default()).unwrap();
        let json_output = resource.to_json_ad().unwrap();
        let in_value: serde_json::Value = serde_json::from_str(json_input).unwrap();
        let out_value: serde_json::Value = serde_json::from_str(&json_output).unwrap();
        assert_eq!(in_value, out_value);
    }

    #[test]
    #[should_panic(expected = "@id must be a strin")]
    fn parse_and_serialize_json_ad_wrong_id() {
        let store = crate::Store::init().unwrap();
        store.populate().unwrap();
        let json_input = r#"{"@id": 5}"#;
        parse_json_ad_resource(json_input, &store, &ParseOpts::default()).unwrap();
    }

    #[test]
    // This test should actually fail, I think, because the datatype should match the property.
    #[should_panic(expected = "Invalid value for Markdown")]
    fn parse_and_serialize_json_ad_wrong_datatype_int_to_str() {
        let store = crate::Store::init().unwrap();
        store.populate().unwrap();
        let json_input = r#"{
            "@id": "https://atomicdata.dev/classes/Agent",
            "https://atomicdata.dev/properties/description": 1
          }"#;
        parse_json_ad_resource(json_input, &store, &ParseOpts::default()).unwrap();
    }

    #[test]
    #[should_panic(expected = "Not a valid Timestamp: 1.124. invalid digit found in string")]
    fn parse_and_serialize_json_ad_wrong_datatype_float() {
        let store = crate::Store::init().unwrap();
        store.populate().unwrap();
        let json_input = r#"{
            "@id": "https://atomicdata.dev/classes/Agent",
            "https://atomicdata.dev/properties/createdAt": 1.124
          }"#;
        parse_json_ad_resource(json_input, &store, &ParseOpts::default()).unwrap();
    }

    // Roundtrip test requires fixing, because the order of imports can get problematic.
    // We should first import all Properties, then Classes, then other things.
    // See https://github.com/atomicdata-dev/atomic-server/issues/614
    #[ignore]
    #[test]
    fn serialize_parse_roundtrip() {
        use crate::Storelike;
        let store1 = crate::Store::init().unwrap();
        store1.populate().unwrap();
        let store2 = crate::Store::init().unwrap();
        let all1: Vec<Resource> = store1.all_resources(true).collect();
        let serialized = crate::serialize::resources_to_json_ad(&all1).unwrap();

        store2
            .import(&serialized, &ParseOpts::default())
            .expect("import failed");
        let all2_count = store2.all_resources(true).count();

        assert_eq!(all1.len(), all2_count);
        let found_shortname = store2
            .get_resource(urls::CLASS)
            .unwrap()
            .get(urls::SHORTNAME)
            .unwrap()
            .clone();
        assert_eq!(found_shortname.to_string(), "class");
    }

    #[test]
    fn parser_should_error_when_encountering_nested_resource() {
        let store = crate::Store::init().unwrap();
        store.populate().unwrap();

        let json = r#"{
            "@id": "https://atomicdata.dev/classes",
            "https://atomicdata.dev/properties/collection/members": [
              {
                "@id": "https://atomicdata.dev/classes/FirstThing",
                "https://atomicdata.dev/properties/description": "Named nested resource"
              },
              {
                "https://atomicdata.dev/properties/description": "Anonymous nested resource"
              },
              "https://atomicdata.dev/classes/ThirdThing"
            ]
          }"#;
        let parsed = parse_json_ad_resource(json, &store, &ParseOpts::default());
        assert!(parsed.is_err(), "Subresource with @id should have errored");
    }

    fn create_store_and_importer() -> (crate::Store, String) {
        let store = crate::Store::init().unwrap();
        store.set_server_url("http://localhost:9883");
        store.populate().unwrap();
        let agent = store.create_agent(None).unwrap();
        store.set_default_agent(agent);
        let mut importer = Resource::new_instance(urls::IMPORTER, &store).unwrap();
        importer.save_locally(&store).unwrap();
        (store, importer.get_subject().into())
    }

    #[test]
    fn import_resource_with_localid() {
        let (store, importer) = create_store_and_importer();

        let local_id = "my-local-id";

        let json = r#"{
            "https://atomicdata.dev/properties/localId": "my-local-id",
            "https://atomicdata.dev/properties/name": "My resource"
          }"#;

        let parse_opts = ParseOpts {
            save: SaveOpts::Commit,
            signer: Some(store.get_default_agent().unwrap()),
            for_agent: ForAgent::Sudo,
            overwrite_outside: false,
            importer: Some(importer.clone()),
        };

        store.import(json, &parse_opts).unwrap();

        let imported_subject = generate_id_from_local_id(&importer, local_id);

        let found = store.get_resource(&imported_subject).unwrap();
        println!("{:?}", found);
        assert_eq!(found.get(urls::NAME).unwrap().to_string(), "My resource");

        // LocalId should be removed from the imported resource
        assert_eq!(found.get(urls::LOCAL_ID).is_err(), true);
    }
    #[test]
    fn import_resource_with_json() {
        let (store, importer) = create_store_and_importer();

        let local_id = "my-local-id";

        let json = r#"
        [
        {
            "@id": "http://localhost:9883/01k06n9cz8r8vsdehh4btz8tdk",
            "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/json",
            "https://atomicdata.dev/properties/description": "Een prop met een json value",
            "https://atomicdata.dev/properties/isA": [
                "https://atomicdata.dev/classes/Property"
            ],
            "https://atomicdata.dev/properties/shortname": "nieuwe-json-prop"
        }, {
            "https://atomicdata.dev/properties/localId": "my-local-id",
            "https://atomicdata.dev/properties/name": "My resource",
            "http://localhost:9883/01k06n9cz8r8vsdehh4btz8tdk": {
                "wat": "patat"
            }
        }
        ]"#;

        let parse_opts = ParseOpts {
            save: SaveOpts::Commit,
            signer: Some(store.get_default_agent().unwrap()),
            for_agent: ForAgent::Sudo,
            overwrite_outside: false,
            importer: Some(importer.clone()),
        };

        store.import(json, &parse_opts).unwrap();

        let imported_subject = generate_id_from_local_id(&importer, local_id);

        let found = store.get_resource(&imported_subject).unwrap();
        assert_eq!(found.get(urls::NAME).unwrap().to_string(), "My resource");

        // LocalId should be removed from the imported resource
        assert_eq!(found.get(urls::LOCAL_ID).is_err(), true);
    }

    #[test]
    fn import_resources_localid_references() {
        let (store, importer) = create_store_and_importer();

        let parse_opts = ParseOpts {
            save: SaveOpts::Commit,
            for_agent: ForAgent::Sudo,
            signer: Some(store.get_default_agent().unwrap()),
            overwrite_outside: false,
            importer: Some(importer.clone()),
        };

        store
            .import(include_str!("../test_files/local_id.json"), &parse_opts)
            .unwrap();

        let reference_subject = generate_id_from_local_id(&importer, "reference");
        let my_subject = generate_id_from_local_id(&importer, "my-local-id");
        let found = store.get_resource(&my_subject).unwrap();
        let found_ref = store.get_resource(&reference_subject).unwrap();

        assert_eq!(
            found.get(urls::PARENT).unwrap().to_string(),
            reference_subject
        );
        assert_eq!(&found_ref.get(urls::PARENT).unwrap().to_string(), &importer);
        assert_eq!(
            found
                .get(urls::WRITE)
                .unwrap()
                .to_subjects(None)
                .unwrap()
                .first()
                .unwrap(),
            &reference_subject
        );
    }

    #[test]
    fn import_resource_malicious() {
        let (store, importer) = create_store_and_importer();
        store.set_server_url("http://localhost:9883");

        // Try to overwrite the main drive with some malicious data
        let agent = store.get_default_agent().unwrap();
        let mut resource = Resource::new_generate_subject(&store).unwrap();
        resource
            .set(
                urls::WRITE.into(),
                vec![agent.subject.clone()].into(),
                &store,
            )
            .unwrap();
        resource.save_locally(&store).unwrap();

        let json = format!(
            r#"{{
            "@id": "{}",
            "https://atomicdata.dev/properties/write": ["https://some-malicious-actor"]
        }}"#,
            resource.get_subject()
        );

        let mut parse_opts = ParseOpts {
            save: SaveOpts::Commit,
            signer: Some(agent.clone()),
            for_agent: agent.subject.into(),
            overwrite_outside: false,
            importer: Some(importer),
        };

        // We can't allow this to happen, so we expect an error
        store.import(&json, &parse_opts).unwrap_err();

        // If we explicitly allow overwriting resources outside scope, we should be able to import it
        parse_opts.overwrite_outside = true;
        store.import(&json, &parse_opts).unwrap();
    }

    #[test]
    fn is_property() {
        let json = r#"
        {
    "https://atomicdata.dev/properties/localId": "newprop",
    "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/string",
    "https://atomicdata.dev/properties/description": "test property",
    "https://atomicdata.dev/properties/isA": [
      "https://atomicdata.dev/classes/Property"
    ],
    "https://atomicdata.dev/properties/shortname": "homepage"
}
        "#;

        let object: serde_json::Value = serde_json::from_str(json).unwrap();

        assert!(
            object_is_property(&object),
            "This JSON should be parsed as a property"
        )
    }

    #[test]
    /// The importer should import properties first
    fn parse_sorted_properties() {
        let (store, importer) = create_store_and_importer();
        store.populate().unwrap();

        let json = r#"[
{
    "https://atomicdata.dev/properties/localId": "test1",
    "https://atomicdata.dev/properties/name": "val"
},
{
"https://atomicdata.dev/properties/localId": "test2"
},
  {
    "https://atomicdata.dev/properties/localId": "newprop",
    "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/string",
    "https://atomicdata.dev/properties/description": "test property",
    "https://atomicdata.dev/properties/isA": [
      "https://atomicdata.dev/classes/Property"
    ],
    "https://atomicdata.dev/properties/parent": "test2",
    "https://atomicdata.dev/properties/shortname": "homepage"
}]"#;

        let parse_opts = crate::parse::ParseOpts {
            for_agent: ForAgent::AgentSubject(store.get_default_agent().unwrap().subject),
            importer: Some(importer.clone()),
            overwrite_outside: false,
            // We sign the importer Commits with the default agent,
            // not the one performing the import, because we don't have their private key.
            signer: Some(store.get_default_agent().unwrap()),
            save: crate::parse::SaveOpts::Commit,
        };

        store.import(json, &parse_opts).unwrap();

        let parent_subject = generate_id_from_local_id(&importer, "test1");
        let found = store.get_resource(&parent_subject).unwrap();
        assert_eq!(found.get(urls::PARENT).unwrap().to_string(), importer);

        let newprop_subject = format!("{importer}/newprop");
        let _prop = store.get_resource(&newprop_subject).unwrap();
    }

    // TODO: Add support for parent sorting in the parser.

    // #[test]
    // fn import_parent_chain() {
    //     let (store, importer) = create_store_and_importer();

    //     let json = r#"[
    // {
    // "https://atomicdata.dev/properties/localId": "test2",
    // "https://atomicdata.dev/properties/parent": "test1"
    // },
    // {
    // "https://atomicdata.dev/properties/localId": "test1"
    // }
    //     ]"#;

    //     let parse_opts = crate::parse::ParseOpts {
    //         for_agent: ForAgent::AgentSubject(store.get_default_agent().unwrap().subject),
    //         importer: Some(importer.clone()),
    //         overwrite_outside: false,
    //         signer: Some(store.get_default_agent().unwrap()),
    //         save: crate::parse::SaveOpts::Commit,
    //     };

    //     store.import(json, &parse_opts).unwrap();

    //     let _test2_resource = store.get_resource(&format!("{importer}/test2")).unwrap();
    // }
}
